package com.epam.training.concurrency.second.task3;

import java.sql.Connection;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Queue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Connection Pool class which work with h2 database;
 * 
 * @author bogdan
 *
 */
public class ConectionPool {
	private static final int MAX_AVAILABLE = 10;
	private final Semaphore available;

	public final static int WAITING_TIMEOUT = 1000;
	public final static int IDLE_TIMEOUT = 1000;
	private static final String DRIVER = "org.h2.Driver";

	private Queue<MyConnection> connections = new ConcurrentLinkedQueue<MyConnection>();
	private Queue<MyConnection> used = new ConcurrentLinkedQueue<MyConnection>();

	private String url;
	private String user;
	private String passwd;

	private final Observer observer;

	/**
	 * Main constructor of connection pool.
	 * 
	 * @param url
	 * @param user
	 * @param passwd
	 */
	ConectionPool(String url, String user, String passwd) {
		available = new Semaphore(MAX_AVAILABLE, true);

		this.url = url;
		this.user = user;
		this.passwd = passwd;

		observer = new Observer();
	}

	/**
	 * Start connection pool
	 * 
	 * @throws SQLException
	 * @throws ClassNotFoundException
	 */
	public void startPool() throws SQLException, ClassNotFoundException {
		Class.forName(DRIVER);
		for (int i = 0; i < MAX_AVAILABLE; i++) {
			connections.add(new MyConnection(DriverManager.getConnection(url,
					user, passwd)));
		}

		observer.start();
	}

	/**
	 * Get connection from pool if it available
	 * 
	 */
	public MyConnection checkOut() throws InterruptedException {
		if (available.tryAcquire(WAITING_TIMEOUT, TimeUnit.NANOSECONDS)) {
			return getNextAvailableItem();
		}

		return null;
	}

	/**
	 * Check connection for exist in pool and if this connection used. If it
	 * used then remove from list for used connection.
	 * 
	 * @param c
	 */
	public void checkIn(MyConnection c) {
		if (markAsUnused(c)) {
			available.release();
		}
	}

	/**
	 * Delete connection from "used" list if it exist
	 * 
	 * @param c
	 * @return
	 */
	private boolean markAsUnused(MyConnection c) {
		if (connections.contains(c) && used.contains(c)) {
			used.remove(c);
		}
		return false;
	}

	private class MyConnection extends WrapConnection {
		private long time;

		public MyConnection(Connection connection) {
			super(connection);
		}

		public void close() {
			checkIn(this);
		}

		@Override
		protected void writeCurrentTime() {
			time = System.nanoTime();
		}

		public long getlastTime() {
			return time;
		}
	}

	/**
	 * Check thread work time and close if it not do anything
	 * 
	 * @author Bogdan
	 *
	 */
	private class Observer implements Runnable {
		private Thread t;
		private volatile boolean run = true;

		Observer() {
			t = new Thread();
			run = true;
		}

		@Override
		public void run() {
			while (run) {
				for (MyConnection conection : used) {
					if ((System.nanoTime() - conection.getlastTime()) >= IDLE_TIMEOUT) {
						conection.close();
						used.remove(conection);
					}
				}
				try {
					TimeUnit.MILLISECONDS.sleep(IDLE_TIMEOUT);
				} catch (InterruptedException e) {
					Logger.getLogger(Observer.class.getName()).log(
							Level.SEVERE, null, e);
				}
			}
		}

		/**
		 * Start observer thread
		 */
		public void start() {
			t.start();
		}

		/**
		 * Stop observer
		 */
		public void stop() {
			run = false;
		}
	}

	/**
	 * Get next available connection
	 * 
	 * @return
	 */
	private synchronized MyConnection getNextAvailableItem() {
		for (MyConnection connection : connections) {
			if (!used.contains(connection)) {
				used.add(connection);
				return connection;
			}
		}

		return null;
	}

	/**
	 * Close all connection in connection pool and shutdown pool
	 * 
	 * @throws SQLException
	 * 
	 */
	public void shutdown() throws SQLException {
		observer.stop();

		for (MyConnection connection : used) {
			connection.close();
		}
		used.clear();

		for (MyConnection connection : connections) {
			if (connection != null) {
				connection.fullClose();
			}
		}
	}
}
